個々の状態をState Machineの図で記述する
参考
タイトル変えようmrsekut.icon
状態遷移図
型のみで
図じゃねえし
課題
状態の遷移に副作用が伴うとき
例えば、条件分岐があって、その条件の判断のためにDB上のデータが必要なとき
状態の遷移に、throw Errorを伴うとき
各状態遷移のための条件に伴う引数が各状態によってまばらなとき
state machineの入れ子があるとき
めちゃくちゃ型安全にするためにはどうするか
実際の状態遷移はもっと複雑になるがそれに対応できるか
例えば、以下のようなことをやりたい
状態遷移の中で分岐がある
A→B→E
A→C→D→E
のようにEに辿り着くために2つの経路があるなど
状態の遷移時にactionを実行したい
例えば、A→Bに遷移する際に、特定のPDFを印刷したいなど
状態の遷移時にvalidationを行いたい
例えば、AからBに遷移するためには、他の場所でこの条件を満たしていけない、という情報を書くなど
仮にAからBに行く時に2つのvalidation checkを行いたい場合は、それらの関数は独立して定義したい
内部のある状態遷移にのみ必要な情報を受け取りたい
例えば、C→Dの遷移の際のみ、あるデータが必要
entityに閉じ込めるなら、IOがあってはならない
ただの状態だけでなく↑こういう副作用も含めて書くと良さそうね
状態遷移が明確になって良い
ここからここへいけない、とかが明確になる
状態をドキュメントに残せるのが良い
曖昧な状態はドキュメントに残しづらいが、この方法なら明示的に残せる
e.g. EmptyCart
全ての取りうる状態に対して考えることを余儀なくされる
型と関数でstate machineを定義する
code:hs
data ShoppingCart = EmptyCart
| ActiveCart ActiveCartData
| PaidCart PaidCartData
data ActiveCartData = ActiveCartData
}
data PaidCartData = PaidCartData
, payment :: Float
}
addItem :: ShoppingCart -> Item -> ShoppingCart
addItem cart item = case cart of
EmptyCart -> ActiveCart $ ActiveCartData item ActiveCart cartData -> ActiveCart $ ActiveCartData $ item : unpaidItems cartData
PaidCart _ -> cart
一つのstate machineに対して、1つの関数を定義する
各状態から、次にどの状態に進むかは、この関数で定義する
entityの根幹だということmrsekut.icon
状態遷移に必要な引数のみをとって、状態遷移のみを行う関数
コレ自体にvalidationなどの処理は含めない
上記関数のPaidCartの分岐の箇所は、状態を何も変更しないことを意味する
p.133
個々のEvnetをUnionでまとめる
新しいEventが増えた時に追加しやすように。
この辺ちゃんと理解していない..mrsekut.icon
なんで急にEventの話になったのかいまいちわかっていないmrsekut.icon
手を動かしなら進めないと何の話をしているのかわからなくなる
tsだとこういう型を用意する
code:ts
// prettier-ignore
type DepFn<Deps extends Function[], Input, Output> =
? Rest extends Function[]
? (dep: Dep) => DepFn<Rest, Input, Output>
: never
: (input: Input) => Output;
外部から値を取得したいからusecaseに書く、という発想はそもそもおかしい
まずどこに書くべきか決まって、そこからそれに必要な値を取得する
だから上記のようにdepにして、entityに書くべき
そこで問題になるのがIOやEitherなどのeffect
AsyncやResultなどのeffectを持っている
これらは一緒に使うことが多いので以下のような型を用意しておくと便利
code:fs
type AsyncResult<'suc, 'fai> Async<Result<'suc, 'fai>>
これらの型は一度使うと、それを内部で使う関数全体がそれになる
あれ、これ、この感じでやっていくのか??mrsekut.icon
一つ前の節では、説明の単純化のためにeffectを無視してただけってこと?
要は、本軸以外の流れは全てdependencyとして扱えば、
全てを同じ見方で統一できる
validationのやつは各stepで作っているが、
それを最終的にどうやって本軸に接続するか
dependencyってなんで関数なん?
その関数を得られた値を渡すのに比べて何が優れている?
内部で組み合わすことができるからか
dep1の結果を使ってdep2を実行できる
これホンマに純粋関数の世界でできるのか?
各stepに沿う関数を作って、連結する
code:fs
let placeOrder unvalidatedOrder =
unvalidatedOrder
|> validateOrder
|> priceOrder
|> acknowledgeOrder
|> createEvents
これやるためには、副作用があったらダメだもんなmrsekut.icon
ほんまにできるのかこれ
これできたらかなりすごいが
inputとoutputが乖離する問題をどうにかして解決しないといけない
別の場所から取ってくる引数、みたいなのをどうにかして用意しないといけない
pipelineの最後にcreateEventsするのかmrsekut.icon
そこまでに完全なorderを作りきって、それを使ってeventを生成する
toAddressというvalidation関数は、通信も必要なので複雑
checkAddressExistsは引数で渡す
toValidatedOrdrLineを定義する
返り値を共通の型になるようにliftしてる
ここでもliftしている
inputとoutputが乖離する問題
adapterを用意する
このページ頭のコードが良い例
toProducCodeはProductCodeを返したい、
が、最終行のcheckProductCodeExistsがboolを返すので困る
checkProductCodeExistsはそれ自体で正しい関数なので、今の都合のために書き換えるわけにも行かない
そこでadapterを使う
code:ts
const predicateToPassthru = <T>(
f: (v: T) => boolean,
x: T,
errorMessage: string,
): T => {
if (!f(x)) {
throw new Error(errorMessage);
}
return x;
};
確かに、こういうpipeline用の関数をいくつか用意しておくと便利そう
実際はf#なので、カリー化されている
errorMessageが第1引数になっている
hsで書くと
code:hs
placeOrder = createEvents . acknowledgeOrder . priceOrder . validateOrder
-- validateOrder :: CheckProductCodeExists -> CheckAddressExists -> UnvalidatedOrder -> Either OrderError ValidatedOrder
validateOrder :: CheckProductCodeExists -> CheckAddressExists -> UnvalidatedOrder -> ValidatedOrder
validateOrder = undefined
-- priceOrder :: GetProductPrice -> ValidatedOrder -> Either PlaceOrderError PricedOrder
priceOrder :: GetProductPrice -> ValidatedOrder -> PricedOrder
priceOrder = undefined
acknowledgeOrder :: CreateOrderAcknowledgmentLetter -> PricedOrder -> Maybe OrderAcknowledgmentSent
acknowledgeOrder = undefined
createEvents :: PricedOrder -> Maybe OrderAcknowledgmentSent -> PlacedOrderEvent createEvents = undefined
depがあるせいで当然composeできない
monadを使わずに解決するう方法
部分適用を使って1引数関数にする
まあわかるが...
まあそもそもここは別にめちゃくちゃこだわるポイントではないmrsekut.icon